package cn.cloudtogo.server.mysql.service;

import cn.cloudtogo.common.Assert;
import cn.cloudtogo.common.GendbException;
import cn.cloudtogo.common.atomapi.ApiDisplay;
import cn.cloudtogo.common.atomapi.ApiListQuery;
import cn.cloudtogo.common.atomapi.ApiMethodModel;
import cn.cloudtogo.common.atomapi.AtomApiHelper;
import cn.cloudtogo.common.atomapi.token.ApiToken;
import cn.cloudtogo.common.model.ApiTokenDao;
import cn.cloudtogo.common.model.ApiTokenModel;
import cn.cloudtogo.common.model.ExternalDatasourceDao;
import cn.cloudtogo.common.model.ExternalDatasourceModel;
import cn.cloudtogo.common.types.Pair;
import cn.cloudtogo.common.types.RequestHeaderObject;
import cn.cloudtogo.common.util.RetryHelper;
import cn.cloudtogo.common.util.StringHelper;
import cn.cloudtogo.server.mysql.controller.TableRowsController;
import cn.cloudtogo.support.mysql.table.TableApi;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import springfox.documentation.annotations.ApiIgnore;

import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author yaolianhua789@gmail.com
 **/
@Component
public class ApiListService implements ApiListQuery {

    private final RequestMappingHandlerMapping mapping;
    private final TableApi tableApi;
    private final ExternalDatasourceDao externalDatasourceDao;
    private final ApiTokenDao apiTokenDao;

    public ApiListService(RequestMappingHandlerMapping mapping,
                          TableApi tableApi,
                          ExternalDatasourceDao externalDatasourceDao,
                          ApiTokenDao apiTokenDao) {
        this.mapping = mapping;
        this.tableApi = tableApi;
        this.externalDatasourceDao = externalDatasourceDao;
        this.apiTokenDao = apiTokenDao;
    }

    private static ApiMethodModel.Response getResponse(ApiOperation apiOperation) {
        ApiMethodModel.Response response = null;
        if (Objects.equals(apiOperation.response(), Integer.class)) {
            response = new ApiMethodModel.Response(apiOperation.code(), "1");

        }
        if (Objects.equals(apiOperation.response(), Void.class)) {
            response = new ApiMethodModel.Response(apiOperation.code(), "");

        }
        if (Objects.equals(apiOperation.response(), List.class)) {
            response = new ApiMethodModel.Response(apiOperation.code(), "[\n" +
                    "  {\n" +
                    "    \"prop1\": {},\n" +
                    "    \"prop2\": {},\n" +
                    "    \"prop3\": {}\n" +
                    "  }\n" +
                    "]");

        }
        if (Objects.equals(apiOperation.response(), Pair.class)) {
            response = new ApiMethodModel.Response(apiOperation.code(), "{\n" +
                    "  \"prop1\": {},\n" +
                    "  \"prop2\": {},\n" +
                    "  \"prop3\": {}\n" +
                    "}");

        }
        return response;
    }

    @Override
    public ApiMethodModel query(String apiKey) {
        String decode = StringHelper.decode(apiKey);

        String[] args = decode.split("\\|");
        Assert.state(args.length == 3, () -> "api key invalid");
        String uuid = args[0];
        String table = args[1];
        String operation = args[2];

        List<ApiDisplay> displays = this.query(uuid, null, table);
        Assert.state(displays.size() == 1, () -> "expected apis size is 1, but actual got " + displays.size());
        ApiMethodModel methodModel = displays.get(0).getMethods()
                .stream()
                .filter(e -> Objects.equals(e.getApiKey(), apiKey))
                .findFirst()
                .orElseThrow(() -> new GendbException("can not got any data for api key [" + apiKey + "]"));

        return methodModel;
    }

    private List<ApiDisplay> handleApiDisplay(List<ApiMethodModel> apis, String uuid, String method, String table) {

        List<ApiDisplay> displays = flattenApis(apis, uuid, table);
        if (StringUtils.hasText(table) && StringUtils.hasText(method)) {
            ApiDisplay apiDisplay = displays
                    .stream()
                    .filter(e -> Objects.equals(table, e.getTable()))
                    .findFirst()
                    .orElseThrow(() -> new GendbException("record does not exist [" + table + "]"));
            List<ApiMethodModel> methodModels = apiDisplay
                    .getMethods()
                    .stream()
                    .filter(e -> method.equalsIgnoreCase(e.getMethod()))
                    .collect(Collectors.toList());
            apiDisplay.setMethods(methodModels);

            return List.of(apiDisplay);
        }
        if (StringUtils.hasText(table) && !StringUtils.hasText(method)) {
            return displays.stream()
                    .filter(e -> Objects.equals(table, e.getTable()))
                    .collect(Collectors.toList());
        }
        if (!StringUtils.hasText(table) && StringUtils.hasText(method)) {
            for (ApiDisplay apiDisplay : displays) {
                List<ApiMethodModel> methodModels = apiDisplay
                        .getMethods()
                        .stream()
                        .filter(e -> method.equalsIgnoreCase(e.getMethod()))
                        .collect(Collectors.toList());
                apiDisplay.setMethods(methodModels);
            }
            return displays;
        }
        return displays;

    }

    private List<ApiDisplay> flattenApis(List<ApiMethodModel> apis, String uuid, String table) {
        ExternalDatasourceModel model = RetryHelper.mayRetry(() -> queryMayRetry(uuid), 10, 100);
        if (model == null) {
            throw new GendbException("Get external datasource null [" + uuid + "]");
        }
        List<String> tables = tableApi.tables(RequestHeaderObject.ofModel(model));

        List<ApiDisplay> apiDisplays = new LinkedList<>();
        if (StringUtils.hasText(table)) {
            if (!tables.contains(table)) {
                throw new GendbException(String.format("table %s.%s does not exist", model.getSchema(), table));
            }
            apiDisplays.add(buildApiDisplayModel(uuid, table, apis));
        } else {
            tables.forEach(e -> apiDisplays.add(buildApiDisplayModel(uuid, e, copyNew(apis))));
        }

        return apiDisplays;
    }

    private ExternalDatasourceModel queryMayRetry(String uuid) throws RetryHelper.RetryException {
        ExternalDatasourceModel model = externalDatasourceDao.query(uuid);
        if (model == null) {
            throw new RetryHelper.RetryException("Get external datasource null [" + uuid + "]");
        }
        return model;
    }

    @Override
    public List<ApiDisplay> query(String uuid, String type, String table) {

        Map<RequestMappingInfo, HandlerMethod> handlerMethodMap = mapping.getHandlerMethods()
                .entrySet()
                .stream()
                .filter(e -> Objects.equals(e.getValue().getBeanType(), TableRowsController.class))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

        List<ApiMethodModel> apis = new LinkedList<>();

        ApiTokenModel model = apiTokenDao.query(uuid, "Global");
        ApiToken globalApiToken = null;
        if (model != null) {
            globalApiToken = ApiToken.of(model.getNotes(), model.getToken(), model.getType(), null);
        }

        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethodMap.entrySet()) {
            RequestMappingInfo mappingInfo = entry.getKey();
            HandlerMethod handlerMethod = entry.getValue();
            Set<String> patterns = Objects.requireNonNull(mappingInfo.getPatternsCondition()).getPatterns();
            String path = patterns.iterator().next();

            Set<RequestMethod> methods = mappingInfo.getMethodsCondition().getMethods();
            String method = methods.iterator().next().name();

            ApiOperation apiOperation = handlerMethod.getMethodAnnotation(ApiOperation.class);
            Assert.notNull(apiOperation, () -> "@ApiOperation is null");
            ApiMethodModel methodModel = new ApiMethodModel();
            methodModel.setMethod(method);
            methodModel.setPath(path);

            methodModel.setDesc(apiOperation.value());
            methodModel.setResponse(getResponse(apiOperation));
            String notes = apiOperation.notes();
            Assert.argument(StringUtils.hasText(notes), () -> "method operation is null");
            methodModel.setNotes(notes);
            methodModel.setToken(globalApiToken);

            if (handlerMethod.hasMethodAnnotation(ApiImplicitParams.class)) {
                ApiImplicitParams apiImplicitParams = handlerMethod.getMethodAnnotation(ApiImplicitParams.class);
                List<ApiMethodModel.Header> headers = Arrays.stream(Objects.requireNonNull(apiImplicitParams).value())
                        .map(this::buildHeader)
                        .collect(Collectors.toList());
                methodModel.setHeaders(headers);
            }

            List<ApiMethodModel.Parameter> parameters = new LinkedList<>();
            for (MethodParameter methodParameter : handlerMethod.getMethodParameters()) {

                if (methodParameter.hasParameterAnnotation(ApiIgnore.class)) {
                    continue;
                }
                if (methodParameter.hasParameterAnnotation(PathVariable.class)) {
                    String value = Objects.requireNonNull(methodParameter.getParameterAnnotation(PathVariable.class)).value();
                    if (ApiDisplay.TABLE.equals(value)) {
                        continue;
                    }
                    if (ApiDisplay.UUID.equals(value)) {
                        continue;
                    }
                }

                ApiMethodModel.Parameter parameter = new ApiMethodModel.Parameter();
                handleParameter(parameter, methodParameter);

                parameters.add(parameter);
            }
            methodModel.setParameters(parameters);

            apis.add(methodModel);
        }

        return handleApiDisplay(apis, uuid, type, table);

    }

    private ApiMethodModel.Header buildHeader(ApiImplicitParam apiImplicitParam) {
        ApiMethodModel.Header header = new ApiMethodModel.Header();
        header.setName(apiImplicitParam.name());
        header.setRequired(apiImplicitParam.required());
        header.setValue(apiImplicitParam.value());
        header.setExample(apiImplicitParam.example());

        return header;
    }

    private ApiDisplay buildApiDisplayModel(String uuid, String table, List<ApiMethodModel> apis) {
        ApiDisplay apiDisplay = new ApiDisplay();
        apiDisplay.setTable(table);
        apiDisplay.setPrefix(String.format("/api/%s", uuid));
        apis.forEach(e -> populatePathVariable(uuid, table, e));
        apis.forEach(e -> populateApiKey(uuid, table, e));
        apis.forEach(e -> e.setNotes(String.format("%s_%s", table, e.getNotes())));
        apiDisplay.setMethods(apis);
        return apiDisplay;
    }

    private void populatePathVariable(String uuid, String table, ApiMethodModel methodModel) {
        String uuidReplace = methodModel.getPath().replace("{" + ApiDisplay.UUID + "}", uuid);
        methodModel.setPath(uuidReplace);

        String tableReplace = methodModel.getPath().replace("{" + ApiDisplay.TABLE + "}", table);
        methodModel.setPath(tableReplace);

        /*
        /api/627a0a1ad67f0e4f7cb633ef/users/{id}
         */
        String pathNoPrefix = tableReplace.replace(String.format("/api/%s", uuid), "");
        methodModel.setSubPath(pathNoPrefix);
    }

    private List<ApiMethodModel> copyNew(List<ApiMethodModel> apis) {
        return apis.stream()
                .map(e -> {
                    ApiMethodModel model = new ApiMethodModel();
                    model.setPath(e.getPath());
                    model.setSubPath(e.getSubPath());
                    model.setMethod(e.getMethod());
                    model.setDesc(e.getDesc());
                    model.setResponse(e.getResponse());
                    model.setNotes(e.getNotes());
                    model.setApiKey(e.getApiKey());
                    model.setToken(e.getToken());
                    model.setHeaders(e.getHeaders());
                    model.setParameters(e.getParameters());
                    return model;
                }).collect(Collectors.toList());
    }

    private void handleParameter(ApiMethodModel.Parameter apiParameter, MethodParameter methodParameter) {

        String parameterDesc;
        String parameterExample;
        String parameterName;
        String parameterType = null;
        boolean required;
        if (methodParameter.hasParameterAnnotation(ApiParam.class)) {
            ApiParam apiParam = Objects.requireNonNull(methodParameter.getParameterAnnotation(ApiParam.class));
            parameterDesc = apiParam.value();
            parameterExample = apiParam.example();
            parameterName = apiParam.name();
            parameterType = apiParam.type();
            required = apiParam.required();

            apiParameter.setDesc(parameterDesc);
            apiParameter.setExample(parameterExample);
            apiParameter.setName(parameterName);
            apiParameter.setRequired(required);
        }

        //query
        if (methodParameter.hasParameterAnnotation(RequestParam.class)) {
            apiParameter.setType(new ApiMethodModel.Type("query", parameterType));
        }

        //path
        if (methodParameter.hasParameterAnnotation(PathVariable.class)) {
            apiParameter.setType(new ApiMethodModel.Type("path", parameterType));
        }
        //body
        if (methodParameter.hasParameterAnnotation(RequestBody.class) &&
                methodParameter.getParameterType().equals(Pair.class)) {
            apiParameter.setType(new ApiMethodModel.Type("body", parameterType));
        }
        if (methodParameter.hasParameterAnnotation(RequestBody.class) &&
                methodParameter.getParameterType().equals(List.class)) {
            ParameterizedType parameterizedType = (ParameterizedType) methodParameter.getParameter().getParameterizedType();
            Class<?> aClass = (Class<?>) parameterizedType.getActualTypeArguments()[0];
            if (aClass.equals(Pair.class)) {
                apiParameter.setType(new ApiMethodModel.Type("body", parameterType));
            }
        }

    }

    private void populateApiKey(String uuid, String table, ApiMethodModel methodModel) {
        String apiKey = AtomApiHelper.generateApiKey(uuid, table, methodModel);
        methodModel.setApiKey(apiKey);
    }
}
